<!-- 虚拟列表2（支持超大数据量）

    解决常规虚拟列表问题点：
    1.单个 div 高度超限；
        拆成多个 chunk，每个 chunk 高度 ≤ 2000 × 50 = 100,000px，远低于浏览器限制
    2.scrollTop 精度丢失
        虽然仍存在，但因 chunk 较小，局部渲染误差可控
    3.无法滚动到底部
        所有 chunk 高度之和 = 总高度，且每个 chunk 是独立定位，滚动行为正常
    4.渲染所有 chunk 的 wrapper高度占位
        每个 chunk 都有一个 <div style="height: xxx">，即使内容为空。这样总高度始终 = totalHeight，不会因 DOM 变化而突变。
    5.仅在可视 chunk 内渲染 item
        节省 DOM 数量，保持性能。
    6.scroll 事件只更新 scrollTop
        不会因为 DOM 结构变化导致浏览器修正 scrollTop。

    根据设定的分组数量和可见区域数量，自动切割分组
-->
<template>
	<div
		ref="containerRef"
		class="virtual-list"
		@scroll="handleScroll"
		:style="{ height: containerHeight + 'px' }"
	>
		<!-- 渲染全部的 chunk -->
		<div
			v-for="chunk in allChunks"
			:key="chunk.startIndex"
			:style="{ height: chunk.height + 'px' }"
		>
			<template v-if="isChunkVisible(chunk)">
				<!-- 仅当 chunk 在可视范围内，才渲染其内部 item
                相对于当前chunk进行定位，随着滚动条更新top
            -->
				<div
					v-for="item in getChunkVisibleItems(chunk)"
					:key="item.index"
					:style="{
						position: 'absolute',
						top: item.top + 'px',
						height: itemHeight + 'px',
						width: '100%',
					}"
				>
					<!-- 插槽传递 item -->
					<slot :item="item.data" :index="item.index"></slot>
				</div>
			</template>
		</div>
	</div>
</template>

<script setup>
import { ref, computed, onMounted } from "vue";

const props = defineProps({
	// 总列表
	list: {
		type: Array,
		required: true,
	},
	// 每行单元的高度（px）
	itemHeight: {
		type: Number,
		default: 50,
	},
	// 容器高度
	containerHeight: {
		type: Number,
		default: 400,
	},
	// 每个 chunk 最多包含多少项（1000~5000）
	chunkSize: {
		type: Number,
		default: 2000,
	},
});

//  创建 ref 对象
const containerRef = ref(null);
// 滚出值
const scrollTop = ref(0);

// 缓冲数量
const bufferSize = 10;

// 列表单元总数
const total = computed(() => {
	return props.list.length;
});
// 总高度
const totalHeight = computed(() => total.value * props.itemHeight);

// 所有 chunks
const allChunks = computed(() => {
	const chunks = [];
	const chunkCount = Math.ceil(total.value / props.chunkSize);

	for (let i = 0; i < chunkCount; i++) {
		const start = i * props.chunkSize;
		const end = Math.min(total.value, start + props.chunkSize);
		chunks.push({
			startIndex: start,
			endIndex: end - 1,
			height: (end - start) * props.itemHeight,
		});
	}

	return chunks;
});

// 可见区域项数量，包含缓冲（向上取整）
const visibleCount = computed(() => {
	return Math.ceil(props.containerHeight / props.itemHeight + bufferSize);
});

// 可见区域的索引
const visibleStartIndex = computed(() => {
	return Math.max(
		0,
		Math.floor(scrollTop.value / props.itemHeight) - bufferSize
	);
});
const visibleEndIndex = computed(() => {
	return Math.min(
		total.value - 1,
		visibleStartIndex.value + visibleCount.value
	);
});

// 计算chunk是否与可见区域相交
const isChunkVisible = (chunk) => {
	return !(
		chunk.endIndex < visibleStartIndex.value ||
		chunk.startIndex > visibleEndIndex.value
	);
};

// 获取当前 chunk 中需要渲染的 items
const getChunkVisibleItems = (chunk) => {
	// item渲染范围
	const start = Math.max(chunk.startIndex, visibleStartIndex.value);
	const end = Math.min(chunk.endIndex, visibleEndIndex.value);

	if (start > end) return [];

	// top 当前item在chunk的相对定位
	return props.list.slice(start, end + 1).map((data, i) => ({
		index: start + i,
		top: (start + i - chunk.startIndex) * props.itemHeight,
		data,
	}));
};

// 滚动计算
const handleScroll = () => {
	if (containerRef.value) {
		// 使用精确滚动值
		scrollTop.value = containerRef.value.scrollTop;
	}
};

// ====================================================================
// ===== 暴露滚动方法 =====
// 滚动到顶部
const scrollToTop = () => {
	if (containerRef.value) {
		containerRef.value.scrollTop = 0;
		scrollTop.value = 0;
	}
};

// 滚动到底部
const scrollToBottom = () => {
	if (containerRef.value && total.value > 0) {
		const maxScroll = totalHeight.value - props.containerHeight;
		containerRef.value.scrollTop = Math.max(0, maxScroll);

		scrollTop.value = containerRef.value.scrollTop;
	}
};

// 对外暴露方法
defineExpose({ scrollToTop, scrollToBottom });
</script>

<style scoped>
.virtual-list {
	position: relative;
	overflow: auto;
}

.virtual-list > div {
	position: relative;
}
</style>
